Introduction
PyloaderV 0.6+ lets you write GTA V mods in real Python 3.12.
Drop a .py file into the pyscript/ folder, launch the game, and your script runs alongside ScriptHookV.
CPython 3.12 embedded High-level API 6649 raw natives
- No Python install needed — the runtime is bundled
- Real Python ecosystem — f-strings, dataclasses,
asyncio,pippackages - Hot-reload — press F9 in-game to reload your scripts
- Two API layers — idiomatic wrappers (
gta.player,gta.world...) plus 6649 raw natives
Installation
Requirements
- GTA V (Legacy or Enhanced)
- Install ScriptHookV (provides
ScriptHookV.dll+dinput8.dll) - Download
pyloaderv-0.7.zipand copy the contents to your GTA V root
Folder Structure
Files directly under pyscript/ are loaded as scripts.
Files inside a folder named Lib (case-insensitive) are not loaded as entry points,
so you can use them as shared modules: from my_helpers import helper.
Your First Script
Create hello.py in pyscript/:
import gta
INTERVAL_MS = 1000 # on_tick runs every second
def on_start():
gta.notify("~g~Hello from Python!")
gta.log("info", "hello.py started")
def on_tick():
gta.log("info", "tick")
def on_key_down(vk, down, shift, ctrl, alt):
if down and vk == 0x70: # F1
gta.notify("~b~F1 pressed")
def on_aborted():
gta.log("info", "hello.py stopped")
Launch the game. After ScriptHookV is ready you should see the green
Hello from Python! ticker. Press F9 anytime to reload all scripts without restarting.
Script Structure
A PyloaderV script can define any of these optional functions and module-level variables:
# Optional: how often on_tick runs, in milliseconds
INTERVAL_MS = 100 # 0 = every frame
# Optional: load order. Lower = earlier.
__priority__ = 100
def on_start():
"""Called once when the script is loaded."""
pass
def on_tick():
"""Called every INTERVAL_MS ms (or every frame if 0).
You can call gta.wait(ms) here to pause."""
pass
def on_key_down(vk, down, shift, ctrl, alt):
"""Keyboard event. Do NOT call gta.wait() here."""
pass
def on_aborted():
"""Called before reload or shutdown.
Use it to clean up entities, blips, custom cameras, etc."""
pass
Callbacks
| Callback | When it runs | Can call gta.wait()? |
|---|---|---|
on_start() | Once, after the script is loaded | No |
on_tick() | Every INTERVAL_MS ms | Yes |
on_key_down(vk, down, shift, ctrl, alt) | On keyboard events | No |
on_aborted() | Before reload / shutdown | No |
Module-level options
| Variable | Type | Description |
|---|---|---|
INTERVAL_MS | int | Tick period in ms. 0 = every frame. |
__priority__ | int | Load and dispatch order; lower = earlier (default 100). |
on_key_down is perfectly valid.
Imports
The PyloaderV API lives entirely under the gta module.
# Built-in module (always available inside the game)
import gta
# High-level wrappers
from gta import player, world, ui
from gta import entity, ped, vehicle
from gta import math as gmath
# Wrapper classes
from gta.entity import Entity
from gta.ped import Ped
from gta.vehicle import Vehicle
from gta.player import Player
# Raw natives (45 namespaces, only import what you need)
from gta.natives.player import PLAYER_PED_ID
from gta.natives.misc import SET_TIME_SCALE
from gta.natives.fire import ADD_EXPLOSION
# The standard library is also available
import random
import time
from dataclasses import dataclass
Player
Basic access
from gta import player as gplayer
me = gplayer.current() # Player object
ped = me.ped # the controlled Ped
Properties
| Member | Description |
|---|---|
me.id | Player slot, usually 0. |
me.ped | The Ped the player is controlling. |
me.wanted_level | Read or write 0..5. |
me.is_dead | Read-only bool. |
me.set_police_ignore(True) | Cops ignore you. |
from gta import player, ui
def on_tick():
me = player.current()
if me.wanted_level >= 5:
ui.notify("~r~5 stars!")
def on_key_down(vk, down, shift, ctrl, alt):
if down and vk == 0x71: # F2
player.current().wanted_level = 0
Peds
Spawning
from gta import world, player
origin = player.current().ped.position
soldier = world.create_ped("s_m_y_marine_01", origin, ped_type=29)
if soldier:
soldier.give_weapon(0xFAD1F743, ammo=999) # carbine rifle
soldier.task_combat(player.current().ped)
create_ped takes a model name or hash, a position
(x, y, z), an optional heading (degrees) and a
ped_type (4 = civ male, 26 = civ default, 29 = military, 30 = cop).
Ped API
| Member | Description |
|---|---|
ped.give_weapon(weapon_hash, ammo=999, hidden=False, equip_now=True) | Give a weapon. |
ped.set_as_cop(is_cop=True) | Toggle cop relationship. |
ped.set_relationship_group(group_hash) | Assign a relationship group. |
ped.task_combat(target, combat_flags=0, threat_flags=16) | Make this ped attack target. |
ped.clear_tasks() | Cancel all current tasks. |
ped.set_into(vehicle, seat=-1) | Teleport into a vehicle. -1 = driver. |
ped.is_shooting / is_jacking | Read-only booleans. |
ped.is_in_vehicle(vehicle=None) | Check membership in any or a specific vehicle. |
ped.dismiss() / ped.delete() | Mark no-longer-needed, or hard-delete. |
Ped also inherits everything from Entity:
position, heading, health, exists, is_dead.
Vehicles
Spawning
from gta import world, player, math as gmath
me = player.current()
front = gmath.add(me.ped.position, (0.0, 5.0, 0.0))
car = world.create_vehicle("adder", front, heading=me.ped.heading)
if car:
car.set_engine(True)
car.place_on_ground()
Vehicle API
| Member | Description |
|---|---|
veh.is_driveable | Read-only bool. |
veh.passenger_count / max_passengers | Read-only ints. |
veh.set_engine(on, instantly=True) | Start or stop the engine. |
veh.place_on_ground() | Snap the vehicle on ground. |
veh.ped_in_seat(seat) | Return a Ped in seat, or None. |
veh.delete() | Force-delete the vehicle. |
Drop the player into the vehicle
player_ped = player.current().ped
player_ped.set_into(car, seat=-1) # -1 = driver
Entities
Entity is the base class of Ped and Vehicle.
| Member | Description |
|---|---|
ent.handle | Raw int handle. |
ent.exists | Read-only bool. |
ent.position | Get or set as (x, y, z). |
ent.heading | Get or set float (degrees). |
ent.health | Get or set int. |
ent.is_dead | Read-only bool. |
ent.dismiss() | Mark as no-longer-needed. |
ent.delete() | Force-delete it now. |
# You can wrap any handle (e.g. one returned by a native) in Entity
from gta.entity import Entity
def tag_at(handle):
e = Entity(handle)
if e.exists:
gta.log("info", f"entity at {e.position}")
World
Listing entities
from gta import world
peds = world.get_all_peds() # list[Ped]
vehicles = world.get_all_vehicles() # list[Vehicle]
If you only need raw int handles:
import gta
ped_handles = gta.get_all_peds() # list[int]
veh_handles = gta.get_all_vehicles()
Models & spawning
| Function | Description |
|---|---|
world.load_model(model, timeout_ms=2500) | Request a model. Returns the hash on success, 0 on failure. |
world.release_model(model) | Tell the engine the model is no longer needed. |
world.create_ped(model, pos, heading=0.0, ped_type=26) | Returns a Ped or None. |
world.create_vehicle(model, pos, heading=0.0) | Returns a Vehicle or None. |
Math & Vectors
Vectors are plain tuples of three floats. gta.math ships small helpers:
from gta import math as gmath
a = (100.0, 200.0, 50.0)
b = (110.0, 200.0, 52.0)
gmath.distance(a, b) # 3D distance
gmath.distance_2d(a, b) # XY distance
gmath.length(a) # vector length
gmath.add(a, b) # tuple sum
gmath.sub(a, b)
gmath.scale(a, 0.5)
gmath.offset_around(a, 5.0, 1.57) # orbit on XY plane
vector3 give a namedtuple("Vector3", "x y z").
It is iterable, indexable and unpackable, so it works with gmath helpers transparently.
UI & Notifications
Quick notifications
import gta
from gta import ui
gta.notify("~g~Loaded")
ui.notify("~y~Same as gta.notify")
ui.show_subtitle("Subtitle text", 2500)
ui.show_help_text("Press ~INPUT_CONTEXT~ to interact")
Color codes
| Code | Color | Code | Color |
|---|---|---|---|
~r~ | Red | ~p~ | Purple |
~g~ | Green | ~o~ | Orange |
~b~ | Blue | ~y~ | Yellow |
~w~ | White | ~n~ | Newline |
Keys & Input
on_key_down receives the Win32 virtual key code plus modifier booleans:
def on_key_down(vk, down, shift, ctrl, alt):
if not down:
return
if vk == 0x70: # F1
gta.notify("F1")
elif vk == 0x75: # F6
gta.notify("F6")
elif ctrl and vk == ord("R"):
gta.notify("Ctrl+R")
Common virtual key codes
| Hex | Key | Hex | Key |
|---|---|---|---|
0x70 | F1 | 0x76 | F7 |
0x71 | F2 | 0x77 | F8 |
0x72 | F3 | 0x78 | F9 |
0x73 | F4 | 0x79 | F10 |
0x74 | F5 | 0x1B | Escape |
0x75 | F6 | 0x20 | Space |
0x60..0x69 | Numpad 0..9 | 0x6D | Numpad minus |
0x41..0x5A | A..Z (use ord("X")) | 0x30..0x39 | 0..9 (top row) |
gta.wait() from on_start, on_key_down or on_aborted.
Set a flag and act in on_tick instead.
Time & Weather
Time and weather natives live in gta.natives.clock and gta.natives.misc.
from gta.natives.clock import SET_CLOCK_TIME, PAUSE_CLOCK
from gta.natives.misc import SET_WEATHER_TYPE_NOW_PERSIST, SET_TIME_SCALE
# Time
SET_CLOCK_TIME(12, 0, 0) # noon
PAUSE_CLOCK(True) # freeze in-game time
# Weather (string presets)
SET_WEATHER_TYPE_NOW_PERSIST("CLEAR")
SET_WEATHER_TYPE_NOW_PERSIST("RAIN")
SET_WEATHER_TYPE_NOW_PERSIST("THUNDER")
SET_WEATHER_TYPE_NOW_PERSIST("XMAS") # snow
# Slow-motion: 0.0..1.0, 1.0 is normal speed
SET_TIME_SCALE(0.5)
Explosions
from gta.natives.fire import ADD_EXPLOSION
# ADD_EXPLOSION(x, y, z, type, damage, audible, invisible, cam_shake)
# type: 2 = molotov, 4 = car, 7 = grenade, 12 = fire, 22 = flare, 29 = blimp
def on_key_down(vk, down, shift, ctrl, alt):
if down and vk == 0x76: # F7
from gta import player
x, y, z = player.current().ped.position
ADD_EXPLOSION(x, y, z + 2.0, 7, 1.0, True, False, 1.0)
Blips
Blips are managed via gta.natives.hud. There is no high-level wrapper yet.
from gta.natives.hud import (
ADD_BLIP_FOR_COORD, ADD_BLIP_FOR_ENTITY,
SET_BLIP_SPRITE, SET_BLIP_COLOUR, SET_BLIP_SCALE,
SET_BLIP_AS_SHORT_RANGE, SET_BLIP_FLASHES,
BEGIN_TEXT_COMMAND_SET_BLIP_NAME, ADD_TEXT_COMPONENT_SUBSTRING_PLAYER_NAME,
END_TEXT_COMMAND_SET_BLIP_NAME, REMOVE_BLIP,
)
_blip = None
def make_blip(x, y, z, name="Marker"):
blip = ADD_BLIP_FOR_COORD(x, y, z)
SET_BLIP_SPRITE(blip, 1) # 1 = standard dot
SET_BLIP_COLOUR(blip, 1) # 1 = red
SET_BLIP_SCALE(blip, 0.85)
SET_BLIP_AS_SHORT_RANGE(blip, False)
SET_BLIP_FLASHES(blip, True)
BEGIN_TEXT_COMMAND_SET_BLIP_NAME("STRING")
ADD_TEXT_COMPONENT_SUBSTRING_PLAYER_NAME(name)
END_TEXT_COMMAND_SET_BLIP_NAME(blip)
return blip
def on_start():
global _blip
_blip = make_blip(100.0, 200.0, 25.0, "My place")
def on_aborted():
global _blip
if _blip is not None:
REMOVE_BLIP(_blip)
_blip = None
Props
Props (barrels, cones, props of any kind) come from gta.natives.object.
from gta import world, player
from gta.entity import Entity
from gta.natives.object import CREATE_OBJECT, DELETE_OBJECT
def spawn_barrel_in_front():
me = player.current()
x, y, z = me.ped.position
# Make sure the model is loaded first
model = world.load_model("prop_barrel_02a")
if not model:
return None
# CREATE_OBJECT(modelHash, x, y, z, isNetwork, netMissionEntity, dynamic)
handle = CREATE_OBJECT(model, x + 2.0, y + 2.0, z, False, False, True)
world.release_model(model)
return Entity(handle) # now you can use .position / .heading / .delete()
Browse models on gta-objects.xyz/objects.
Audio
Play UI sounds with gta.natives.audio.
from gta.natives.audio import (
PLAY_SOUND_FRONTEND, GET_SOUND_ID,
STOP_SOUND, RELEASE_SOUND_ID,
)
# Common UI sounds
PLAY_SOUND_FRONTEND(-1, "SELECT", "HUD_FRONTEND_DEFAULT_SOUNDSET", True)
PLAY_SOUND_FRONTEND(-1, "NAV_UP_DOWN", "HUD_FRONTEND_DEFAULT_SOUNDSET", True)
PLAY_SOUND_FRONTEND(-1, "ERROR", "HUD_FRONTEND_DEFAULT_SOUNDSET", True)
PLAY_SOUND_FRONTEND(-1, "CANCEL", "HUD_FRONTEND_DEFAULT_SOUNDSET", True)
# A sound you can stop later
sid = GET_SOUND_ID()
PLAY_SOUND_FRONTEND(sid, "SELECT", "HUD_FRONTEND_DEFAULT_SOUNDSET", True)
# ...later:
STOP_SOUND(sid)
RELEASE_SOUND_ID(sid)
Animations
Play an animation on a Ped. Always request the dictionary first and wait for it.
import gta
from gta import player
from gta.natives.streaming import REQUEST_ANIM_DICT, HAS_ANIM_DICT_LOADED
from gta.natives.task import TASK_PLAY_ANIM, STOP_ANIM_TASK
def play_anim(dict_name, anim_name):
REQUEST_ANIM_DICT(dict_name)
for _ in range(40): # up to 2 seconds
if HAS_ANIM_DICT_LOADED(dict_name):
break
gta.wait(50)
ped = player.current().ped.handle
# TASK_PLAY_ANIM(ped, dict, anim, blendIn, blendOut, duration, flag, rate, lockX, lockY, lockZ)
TASK_PLAY_ANIM(ped, dict_name, anim_name, 8.0, -8.0, -1, 1, 0.0, False, False, False)
def stop_anim(dict_name, anim_name):
STOP_ANIM_TASK(player.current().ped.handle, dict_name, anim_name, 3.0)
Cameras
Custom cameras live in gta.natives.cam.
from gta.natives.cam import (
CREATE_CAM_WITH_PARAMS, RENDER_SCRIPT_CAMS,
SET_CAM_ACTIVE, DESTROY_CAM, SET_CAM_FOV, SET_CAM_COORD, SET_CAM_ROT,
)
_cam = None
def enable_cam(x, y, z, rx, ry, rz, fov=50.0):
global _cam
_cam = CREATE_CAM_WITH_PARAMS("DEFAULT_SCRIPTED_CAMERA",
x, y, z, rx, ry, rz, fov, True, 2)
SET_CAM_ACTIVE(_cam, True)
RENDER_SCRIPT_CAMS(True, False, 0, True, False, 0)
def disable_cam():
global _cam
RENDER_SCRIPT_CAMS(False, False, 0, True, False, 0)
if _cam is not None:
DESTROY_CAM(_cam, False)
_cam = None
def on_aborted():
disable_cam() # always restore gameplay camera
Raycasting
Cast a ray and read the result with gta.natives.shapetest.
from gta import player
from gta.natives.shapetest import (
START_SHAPE_TEST_RAY, GET_SHAPE_TEST_RESULT,
)
def ray_in_front_of_player(distance=50.0):
me = player.current().ped
sx, sy, sz = me.position
# Quick approx: shoot along the screen forward by stepping heading
fx = 0.0; fy = 1.0; fz = 0.0 # override with your forward vector if needed
ex = sx + fx * distance
ey = sy + fy * distance
ez = sz + fz * distance
# flags: -1 = everything, 1 = world, 2 = vehicles, 4 = peds, 8 = objects, 16 = water
handle = START_SHAPE_TEST_RAY(sx, sy, sz, ex, ey, ez, -1, me.handle, 7)
# GET_SHAPE_TEST_RESULT returns: status, hit_bool, end_pos, surface_normal, entity_hit
status, did_hit, end_pos, normal, hit_entity = GET_SHAPE_TEST_RESULT(handle)
return did_hit, end_pos, hit_entity
gta.natives.entity.GET_ENTITY_FORWARD_VECTOR.
Model loading
Some models are not in memory until you request them.
from gta import world
def spawn_safe(model, pos, heading=0.0):
h = world.load_model(model) # request + wait, returns 0 on failure
if not h:
gta.notify(f"~r~Model failed: {model}")
return None
veh = world.create_vehicle(model, pos, heading)
world.release_model(model) # let the engine free it
return veh
If you prefer the raw layer:
from gta.natives.streaming import (
REQUEST_MODEL, HAS_MODEL_LOADED,
IS_MODEL_VALID, IS_MODEL_IN_CDIMAGE,
SET_MODEL_AS_NO_LONGER_NEEDED,
)
from gta.natives.misc import GET_HASH_KEY
def load_model_raw(name, timeout_ms=2500):
h = GET_HASH_KEY(name)
if not IS_MODEL_VALID(h) or not IS_MODEL_IN_CDIMAGE(h):
return 0
REQUEST_MODEL(h)
for _ in range(timeout_ms // 50):
if HAS_MODEL_LOADED(h):
return h
gta.wait(50)
return 0
load_model / REQUEST_MODEL with
release_model / SET_MODEL_AS_NO_LONGER_NEEDED after spawning.
Native Functions
PyloaderV ships 6649 auto-generated native bindings across 45 namespaces. Importing a native is a normal Python import:
from gta.natives.player import PLAYER_PED_ID, GET_PLAYER_WANTED_LEVEL
from gta.natives.entity import GET_ENTITY_COORDS
from gta.natives.misc import GET_HASH_KEY, SET_TIME_SCALE
def on_tick():
ped = PLAYER_PED_ID()
pos = GET_ENTITY_COORDS(ped, True)
wanted = GET_PLAYER_WANTED_LEVEL(0)
SET_TIME_SCALE(0.5)
Common namespaces
playerpedvehicleentity weapontaskstreamingmisc hudgraphicscamaudio cutscenenetworkstatsmoney
Return Types
The wrappers in gta.natives.* already pick the correct return type for you.
You only need to specify return_type when you call a native by hash with gta.invoke.
return_type | Python value | Use for |
|---|---|---|
"void" | None | Natives that return nothing. |
"bool" | bool | Boolean flags. |
"int" / "handle" / "hash" | int | Signed values, entity handles, hashes. |
"uint" / "ulong" | int | Unsigned values (model hashes for example). |
"float" | float | Floating-point natives. |
"str" | str | None | Natives returning a C string. |
"vector3" | Vector3(x, y, z) | Coordinates and rotations. |
import gta
# Same call as PLAYER_PED_ID, but spelled out
ped = gta.invoke(0xD80958FC74E988A6, return_type="handle")
return_type on a float / vector3 / string native, the result will be wrong.
High-level vs raw
You can mix the two layers freely. The wrappers exist to make common things short, the natives to make everything possible.
import gta
from gta.natives.player import PLAYER_PED_ID
from gta.natives.entity import (
GET_ENTITY_COORDS, GET_ENTITY_HEADING)
from gta.natives.misc import GET_HASH_KEY
from gta.natives.streaming import (
REQUEST_MODEL, HAS_MODEL_LOADED,
SET_MODEL_AS_NO_LONGER_NEEDED)
from gta.natives.vehicle import CREATE_VEHICLE
def spawn_adder():
ped = PLAYER_PED_ID()
coords = GET_ENTITY_COORDS(ped, True)
heading = GET_ENTITY_HEADING(ped)
h = GET_HASH_KEY("adder")
REQUEST_MODEL(h)
while not HAS_MODEL_LOADED(h):
gta.wait(50)
veh = CREATE_VEHICLE(h,
coords.x + 3.0, coords.y, coords.z,
heading, False, False, False)
SET_MODEL_AS_NO_LONGER_NEEDED(h)
return veh
from gta import world, player, math as gmath
def spawn_adder():
me = player.current()
pos = gmath.add(me.ped.position, (3.0, 0.0, 0.0))
car = world.create_vehicle("adder", pos,
heading=me.ped.heading)
if car:
car.set_engine(True)
car.place_on_ground()
return car
- The wrapper does not expose a flag you need.
- You are calling something niche (network, social club, replay, etc.).
- You want to inline a single hot call and skip wrapper overhead.
Pip packages
PyloaderV embeds real CPython 3.12, so packages from PyPI work (as long as they support Python 3.12 / Windows x64).
- Install Python 3.12 anywhere on your machine (only used as a tool here).
- Install the package targeting the embedded runtime:
py -3.12 -m pip install --target "<GTAV>\shvpy_runtime\Lib\site-packages" numpy
py -3.12 -m pip install --target "<GTAV>\shvpy_runtime\Lib\site-packages" requests
Now you can import numpy from any script.
on_tick.
Configuration
Edit PyloaderV.ini next to PyloaderV.asi:
[PyloaderV]
# Folder (relative to GTAV root) where .py scripts live.
ScriptsDir=pyscript
# Optional: absolute path to embedded Python distribution.
# Leave empty to use the runtime sitting next to PyloaderV.asi.
PythonHome=
# Key to force-reload all scripts.
ReloadKey=F9
# Hot-reload scripts when their .py file changes.
AutoReload=false
HotReloadInterval=2
# Print debug notifications in-game.
DebugMode=false
# Logging.
LogEnabled=true
LogMaxSizeKB=1024
# Abort a script if a single tick runs longer than this (ms).
ScriptTimeoutMs=5000
AutoReload=true while developing. PyloaderV will reload your script as soon as you save the file.
Logs & Debugging
Runtime messages are written to PyloaderV.log next to PyloaderV.asi.
import gta
gta.log("debug", "detailed info")
gta.log("info", "normal message")
gta.log("warn", "something unusual")
gta.log("error", "something failed")
# print() is also redirected to the log
print("normal print goes to PyloaderV.log too")
on_tick won’t kill the rest of your scripts.
Performance
- Use
INTERVAL_MSaggressively.0means every frame and is rarely necessary. - Common values:
50,100,250,1000. - Cache references in
on_tick: storeme = player.current()instead of calling it twice. - Avoid hundreds of native calls per frame.
- Import only the natives you need.
- Remove diagnostic / verbose scripts before testing performance.
FAQ & Troubleshooting
My script doesn’t run!
- Confirm the file is directly under
pyscript/. - Filenames starting with
__are skipped. - Files inside a
Libfolder are libraries, not entry points. - Check
PyloaderV.log: load failures print the full traceback there.
I get “module not found”
- Imports are
from gta import player, notimport GTA. PyloaderV is a Python module, not a .NET namespace. - Shared modules go in
pyscript/Lib/. - For pip-installed packages, install them into
shvpy_runtime\Lib\site-packages\.
NumPy / requests / external libs?
- Yes — PyloaderV uses real CPython 3.12, so any Windows-x64 / Python-3.12 wheel works.
- Install with
--target "...\shvpy_runtime\Lib\site-packages"(see the Pip section).
How do I clean up properly?
- Use
on_aborted()to delete blips, dismiss spawned peds/vehicles, restore camera. - Track everything you spawn in a list so the cleanup is easy.
Can I use multiple script files?
- Yes. Every
.pydirectly underpyscript/is its own script with its ownon_start/on_tick. - Use
__priority__to control load order if it matters.
Autocomplete in VS Code?
Open for devs (only)/example-project/ in VS Code, install the Python extension, then type gta. in my_script.py.
How do I see “Loaded N script(s)”?
It shows up as a top-left ticker once the game loads. The same line is printed in PyloaderV.log.